'use strict';

const path = require('path');
const fs = require('fs');
const debug = require('debug')('egg-rbac-plugin');
const mongooseStorage = require('./mongoose/index');

class rbac {
  /**
   * @constructs role
   * @param {object} app eggjs application object
   */
  constructor(app) {
    this.mongooseConfig = app.config.mongoose;
    this.config = app.config.rbac;
    this.storage = new mongooseStorage(app);
    if (app.config.rbac.initOnStart) {
      app.beforeStart(() => {
        const appRbacFilePath = path.join(app.baseDir, 'config/rbac.js');
        debug('app rbac config file path %s file path exit %s ', appRbacFilePath, fs.existsSync(appRbacFilePath));
        if (fs.existsSync(appRbacFilePath)) {
          const data = require(appRbacFilePath);
          return this.initData(data.permissions, data.roles, data.groups);
        }
      });
    }
  }

  /**
   * before start init permissions and roles and groups
   * @method rbac#initData
   * @param {object[]} permissions - permission item array
   * @param {string} permissions[].name - permission name
   * @param {string} permissions[].alias - permission alias
   * @param {object[]} roles - role item array
   * @param {string} roles[].name - role name
   * @param {string} roles[].alias - role alias
   * @param {object[]} groups - group item array
   * @param {string} groups[].name - group name
   * @param {string} groups[].alias - group alias
   * @return {object} error object
   * @yields {boolean}
   */
  initData(permissions, roles, groups) {
    if (!permissions || permissions.length === 0) {
      throw new Error('[egg-rbac-plugin] initData parameter permissions is undefined');
    }
    if (!roles || roles.length === 0) {
      throw new Error('[egg-rbac-plugin] initData parameter roles is undefined');
    }
    if (!groups || groups.length === 0) {
      throw new Error('[egg-rbac-plugin] initData parameter groups is undefined');
    }
    debug('init data permissions.length %O roles.length %O groups.length %O', permissions.length, roles.length, groups.length);

    return this._initPermissions(permissions)
      .then(() => this._initRole(roles))
      .then(() => this._initGroup(groups));
  }

  /**
   * Initialize permission
   * @method rbac#_initPermissions
   * @private
   * @param {object[]} permissions - permission item array
   * @param {string} permissions[].name - permission name
   * @param {string} permissions[].alias - permission alias
   * @return {string[]|null} null or ObjectId array
   */
  _initPermissions(permissions) {
    debug('init permission permission type is %s', typeof permissions);
    return this.getAllPermissions()
      .then(oldPermission => {
        const oldPermissionObj = {};
        oldPermission.forEach(item => {
          oldPermissionObj[item.name] = item;
        });
        permissions = permissions.filter(item => {
          if (oldPermissionObj[item.name]) {
            return false;
          }
          return true;
        });
        if (permissions.length === 0) {
          return [];
        }
        return this.storage.insertManyPermission(permissions);
      });
  }

  /**
   * Initialize roles
   * @method rbac#_initRole
   * @private
   * @param {object[]} roles - role item array
   * @param {string} roles[].name - role name
   * @param {string} roles[].alias - role alias
   * @return {object} promise
   */
  _initRole(roles) {
    const arr = roles.map(roleData => {
      return Promise.all([
        this.storage.getPermissionsByNames(roleData.permissions).then(permissions => permissions.map(per => per._id)),
        this.storage.getRoleByName(roleData.name),
      ]).then(([ ids, role ]) => {
        debug('init role %s ', role);
        if (role === null) {
          roleData.permissions = ids;
          return this.newRole(roleData);
        }
        return this.addPermission(role._id, ids);
      });
    });
    return Promise.all(arr);
  }

  /**
   * Initialize groups
   * @method rbac#_initGroup
   * @private
   * @param {object[]} groups - groups item array
   * @param {string} groups[].name - groups name
   * @param {string} groups[].alias - groups alias
   * @return {object} promise
   */
  _initGroup(groups) {
    const arr = groups.map(groupData => {
      return Promise.all([
        this.storage.getRolesByNames(groupData.roles).then(roles => roles.map(per => per._id)),
        this.storage.getGroupByName(groupData.name),
      ]).then(([ ids, group ]) => {
        debug('init role %s ', group);
        if (group === null) {
          groupData.roles = ids;
          return this.newGroup(groupData);
        }
        return this.addRole(group._id, ids);
      });
    });
    return Promise.all(arr);
  }

  /**
   * @method rbac#newRole
   * @param {object} options role info
   * @param {string} options.name - role short name
   * @param {string} options.alias - role full name such as chinese name
   * @param {string[]} options.permissions - string such as mongodb ObjectId
   * @return {object} promise
   */
  newRole({ name, alias, permissions }) {
    if (!name) {
      return new Error('[egg-rbac-plugin] newRole parameter name is undefined');
    }
    if (!alias) {
      return new Error('[egg-rbac-plugin] newRole parameter alias is undefined');
    }
    debug('new role name %s alias %s', name, alias);
    return this.storage.newRole({ name, alias, permissions });
  }

  /**
   * @method rbac#newGroup
   * @param {object} options role info
   * @param {string} options.name - role short name
   * @param {string} options.alias - role full name such as chinese name
   * @param {string[]} options.roles - string such as mongodb ObjectId
   * @return {object} promise
   */
  newGroup({ name, alias, roles }) {
    if (!name) {
      return new Error('[egg-rbac-plugin] newGroup parameter name is undefined');
    }
    if (!alias) {
      return new Error('[egg-rbac-plugin] newGroup parameter alias is undefined');
    }
    debug('new role name %s alias %s', name, alias);
    return this.storage.newGroup({ name, alias, roles });
  }

  /**
   * @method rbac#newPermission
   * @param {object} options role info
   * @param {string} options.name - permission short name
   * @param {string} options.alias - permission full name such as chinese name
   * @param {String} options.category - category of permission short name
   * @param {String} options.categoryAlias - category of permission full name such as chinese name
   * @return {object} promise
   */
  newPermission({ name, alias, category, categoryAlias }) {
    if (!name) {
      return new Error('[egg-rbac-plugin] newPermission parameter name is undefined');
    }
    if (!alias) {
      return new Error('[egg-rbac-plugin] newPermission parameter alias is undefined');
    }
    if (!category) {
      return new Error('[egg-rbac-plugin] newPermission parameter category is undefined');
    }
    if (!categoryAlias) {
      return new Error('[egg-rbac-plugin] newPermission parameter categoryAlias is undefined');
    }
    debug('new permission name %s alias %s category %s categoryAlias %s', name, alias, category, categoryAlias);
    return this.storage.newPermission({ name, alias, category, categoryAlias });
  }

  /**
   * @method rbac#addPermission
   * @param {string} _id - role id
   * @param {string[]} permissionIds - permission ids
   * @return {object} promise
   */
  addPermission(_id, permissionIds) {
    if (!_id) {
      return new Error('[egg-rbac-plugin] addPermission parameter _id is undefined');
    }
    if (!permissionIds || typeof permissionIds !== 'object' || permissionIds.length === 0) {
      return new Error('[egg-rbac-plugin] addPermission parameter permissionIds is undefined');
    }
    debug('new permission name %s alias %s', _id, permissionIds);
    return this.storage.addPermission(_id, permissionIds);
  }

  /**
   * @method rbac#addRole
   * @param {string} _id - group id
   * @param {string[]} roleIds - role ids
   * @return {object} promise
   */
  addRole(_id, roleIds) {
    if (!_id) {
      return new Error('[egg-rbac-plugin] addRole parameter _id is undefined');
    }
    if (!roleIds || typeof roleIds !== 'object' || roleIds.length === 0) {
      return new Error('[egg-rbac-plugin] addPermission parameter roleIds is undefined');
    }
    debug('new permission name %s alias %s', _id, roleIds);
    return this.storage.addRole(_id, roleIds);
  }

  /**
   * @method rbac#removeRolePermissions
   * @param {string} _id - role id
   * @param {string[]} permissionIds - permission ids
   * @return {object} promise
   */
  removeRolePermissions(_id, permissionIds) {
    if (!_id) {
      return new Error('[egg-rbac-plugin] removeRolePermissions parameter _id is undefined');
    }
    if (!permissionIds || typeof permissionIds !== 'object' || permissionIds.length === 0) {
      return new Error('[egg-rbac-plugin] removeRolePermissions parameter permissionIds is undefined');
    }
    debug('new permission name %s alias %s', _id, permissionIds);
    return this.storage.removeRolePermissions(_id, permissionIds);
  }

  /**
   * @method rbac#removeGroupRoles
   * @param {string} _id - role id
   * @param {string[]} roleIds - role ids
   * @return {object} promise
   */
  removeGroupRoles(_id, roleIds) {
    if (!_id) {
      return new Error('[egg-rbac-plugin] removeGroupRoles parameter _id is undefined');
    }
    if (!roleIds || typeof roleIds !== 'object' || roleIds.length === 0) {
      return new Error('[egg-rbac-plugin] removeGroupRoles parameter roleIds is undefined');
    }
    debug('new permission name %s alias %s', _id, roleIds);
    return this.storage.removeGroupRoles(_id, roleIds);
  }

  /**
   * @method rbac#removeGroup
   * @param {string} _id - role _id
   * @return {object} promise
   */
  removeGroup(_id) {
    if (!_id) {
      return new Error('[egg-rbac-plugin] removeGroup parameter _id is undefined');
    }
    return this.storage.removeGroup(_id);
  }

  /**
   * @method rbac#removeRole
   * @param {string} _id - role _id
   * @return {object} promise
   */
  removeRole(_id) {
    if (!_id) {
      return new Error('[egg-rbac-plugin] removeRole parameter _id is undefined');
    }
    return this.storage.removeRole(_id);
  }

  /**
   * @method rbac#removePermission
   * @param {string} _id - permission _id
   * @return {object} promise
   */
  removePermission(_id) {
    if (!_id) {
      return new Error('[egg-rbac-plugin] removePermission parameter _id is undefined');
    }
    return this.storage.removePermission(_id);
  }

  /**
   * @method rbac#modifyRoleAlias
   * @param {string} _id - role _id
   * @param {string} alias - new alias string
   * @return {object} promise
   */
  modifyRoleAlias(_id, alias) {
    if (!_id) {
      return new Error('[egg-rbac-plugin] modifyRoleAlias parameter _id is undefined');
    }
    if (!alias) {
      return new Error('[egg-rbac-plugin] modifyRoleAlias parameter alias is undefined');
    }
    return this.storage.modifyRoleAlias(_id, alias);
  }

  /**
   * @method rbac#modifyGroupAlias
   * @param {string} _id - role _id
   * @param {string} alias - new alias string
   * @return {object} promise
   */
  modifyGroupAlias(_id, alias) {
    if (!_id) {
      return new Error('[egg-rbac-plugin] modifyGroupAlias parameter _id is undefined');
    }
    if (!alias) {
      return new Error('[egg-rbac-plugin] modifyGroupAlias parameter alias is undefined');
    }
    return this.storage.modifyGroupAlias(_id, alias);
  }

  /**
   * @method rbac#modifyPermissionAlias
   * @param {string} _id - role _id
   * @param {string} alias - new alias string
   * @return {object} promise
   */
  modifyPermissionAlias(_id, alias) {
    if (!_id) {
      return new Error('[egg-rbac-plugin] modifyPermissionAlias parameter _id is undefined');
    }
    if (!alias) {
      return new Error('[egg-rbac-plugin] modifyPermissionAlias parameter alias is undefined');
    }
    return this.storage.modifyPermissionAlias(_id, alias);
  }

  /**
   * @method rbac#modifyPermissionCategory
   * @param {string} _id - role _id
   * @param {string} category - new category string
   * @param {string} categoryAlias - new categoryAlias string
   * @return {object} promise
   */
  modifyPermissionCategory(_id, category, categoryAlias) {
    if (!_id) {
      return new Error('[egg-rbac-plugin] modifyPermissionCategory parameter _id is undefined');
    }
    if (!category) {
      return new Error('[egg-rbac-plugin] modifyPermissionCategory parameter category is undefined');
    }
    if (!categoryAlias) {
      return new Error('[egg-rbac-plugin] modifyPermissionCategory parameter categoryAlias is undefined');
    }
    return this.storage.modifyPermissionCategory(_id, category, categoryAlias);
  }

  /**
   * @method rbac#getRolePermission
   * @param {string} name - role name
   * @return {object} promise
   */
  getRolePermission(name) {
    if (!name || typeof name !== 'string') {
      return new Error('[egg-rbac-plugin] getRolePermission parameter name must string');
    }
    debug('get role permission role name is %s', name);
    return this.storage.getRoleByName(name)
      .then(role => role.permissions);
  }

  /**
   * @method rbac#getGroupPermission
   * @param {string} name - group name
   * @return {object} promise
   */
  getGroupPermission(name) {
    if (!name || typeof name !== 'string') {
      return new Error('[egg-rbac-plugin] getGroupPermission parameter name must string');
    }
    debug('get group permission group name is %s', name);
    return this.storage.getGroupPermission(name);
  }

  /**
   * @method rbac#getAllPermissions
   * @return {object} promise
   */
  getAllPermissions() {
    return this.storage.getAllPermissions();
  }

  /**
   * @method rbac#getPermissionsByQuery
   * @param {object} query
   * @param {object} option
   * @return {object} promise
   */
  getPermissionsByQuery(query, option) {
    return this.storage.getPermissionsByQuery(query, option);
  }

  /**
   * @method rbac#getPermissionsTotal
   * @param {} query query
   * @return {object} promise
   */
  getPermissionsTotal(query) {
    return this.storage.getPermissionsTotal(query? query: {});
  }

  /**
   * @method rbac#getAllPermissionsGroupByCategory
   * @return {object} promise
   */
  getAllPermissionsGroupByCategory(style) {
    return this.storage.getAllPermissionsGroupByCategory(style);
  }

  /**
   * @method rbac#getRoleByName
   * @param {string} name role name
   * @return {object} promise
   */
  getRoleByName(name) {
    if (!name || typeof name !== 'string') {
      return new Error('[egg-rbac-plugin] getRoleByName parameter name must string');
    }
    return this.storage.getRoleByName(name);
  }

  /**
   * @method rbac#getAllRoles
   * @return {object} promise
   */
  getAllRoles() {
    return this.storage.getAllRoles();
  }

  /**
   * @method rbac#getRolesByQuery
   * @param {object} query
   * @param {object} option
   * @return {object} promise
   */
  getRolesByQuery(query, option) {
    return this.storage.getRolesByQuery(query, option);
  }

  /**
   * @method rbac#getRolesTotal
   * @param {} query query
   * @return {object} promise
   */
  getRolesTotal(query) {
    return this.storage.getRolesTotal(query? query: {});
  }

  /**
   * @method rbac#getGroupByName
   * @param {string} name group name
   * @return {object} promise
   */
  getGroupByName(name) {
    if (!name || typeof name !== 'string') {
      return new Error('[egg-rbac-plugin] getGroupByName parameter name must string');
    }
    return this.storage.getGroupByName(name);
  }

  /**
   * @method rbac#getAllRoles
   * @return {object} promise
   */
  getAllGroups() {
    return this.storage.getAllGroups();
  }

  /**
   * @method rbac#getGroupsByQuery
   * @param {object} query
   * @param {object} option
   * @return {object} promise
   */
  getGroupsByQuery(query, option) {
    return this.storage.getGroupsByQuery(query, option);
  }

  /**
   * @method rbac#getGroupsTotal
   * @param {} query query
   * @return {object} promise
   */
  getGroupsTotal(query) {
    return this.storage.getGroupsTotal(query? query: {});
  }

  /**
   * @method rbac#can
   * @param {string} permissionName - permission name
   * @return {function} middleware function
   */
  can(permissionName) {
    return async function(ctx, next) {
      // this is instance of Context
      if (ctx.group && ctx.group.can && ctx.group.can(permissionName)) {
        await next();
      } else {
        // https://tools.ietf.org/html/rfc2616#page-66
        ctx.status = 401; // 'Unauthorized'
      }
    };
  }

  /**
   * @method rbac#canAny
   * @param {string[]} permissionNames - permission names
   * @return {function} middleware function
   */
  canAny(permissionNames) {
    return async function(ctx, next) {
      // this is instance of Context
      if (ctx.group && ctx.group.canAny && ctx.group.canAny(permissionNames)) {
        await next();
      } else {
        // https://tools.ietf.org/html/rfc2616#page-66
        ctx.status = 401; // 'Unauthorized'
      }
    };
  }

  /**
   * @method rbac#canAll
   * @param {string[]} permissionNames - permission names
   * @return {function} middleware function
   */
  canAll(permissionNames) {
    return async function(ctx, next) {
      // this is instance of Context
      if (ctx.group && ctx.group.canAll && ctx.group.canAll(permissionNames)) {
        await next();
      } else {
        // https://tools.ietf.org/html/rfc2616#page-66
        ctx.status = 401; // 'Unauthorized'
      }
    };
  }
}

let singleton = null;

module.exports = exports = function(app) {
  if (singleton === null) {
    singleton = new rbac(app);
  }
  return singleton;
};